//! ## Client C APIs for Routing in CS2 System
//!
//! This module provides C APIs for the routing in the CS2 system.

use std::ffi;

use cs2s::{
    cache::{CacheTableKeyRef, CacheTableVal},
    route::LookupRouter,
    sign::CodeSign,
};

use crate::retval;

/// Lookup router client handle pointer.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LookupRouterPtr(*mut ffi::c_void);

/// Safety: The pointer can be transferred between threads.
unsafe impl Send for LookupRouterPtr {}

/// Create a new lookup router.
///
/// ## Safety
///
/// The provided pointers must be valid, otherwise it causes undefined behavior.
/// - `prefix` must be a valid pointer to a null-terminated UTF-8 C string.
/// - `handle_ptr` must be a valid pointer.
///
/// ## Returns
///
/// - `0` on success.
/// - `-EINVAL` if the prefix is invalid.
#[export_name = "cs2s_ro_create"]
pub unsafe extern "C" fn create(
    prefix: *const ffi::c_char,
    handle_ptr: *mut LookupRouterPtr,
) -> ffi::c_int {
    if prefix.is_null() {
        return -libc::EINVAL;
    }

    // Safety: The pointer is guaranteed to be valid C string by the caller.
    let Ok(prefix) = unsafe { ffi::CStr::from_ptr(prefix) }.to_str() else {
        return -libc::EINVAL;
    };

    let router = LookupRouter::new(prefix);

    // Safety: The pointer is guaranteed to be valid by the caller.
    unsafe {
        handle_ptr.write(LookupRouterPtr(Box::into_raw(Box::new(router)) as _));
    }

    0
}

/// Attach the paths to the router.
///
/// ## Safety
///
/// The provided pointers must be valid, otherwise it causes undefined behavior.
/// - `router` must be created by [`create`].
/// - `paths` must be null or a valid array of UTF-8 C strings.
/// - `paths_len` must be the length of the `paths` array.
///
/// ## Returns
///
/// - `0` on success.
/// - `-EINVAL` if the paths are invalid:
///   - If `paths` is not null, but the array contains a null pointer.
///   - If `paths` is not null, but the C string is not valid UTF-8.
///
/// The error code could also come from the underlying implementation, i.e. from
/// [`LookupRouter::attach`].
#[export_name = "cs2s_ro_attach"]
pub unsafe extern "C" fn attach(
    router: LookupRouterPtr,
    paths: *const *const ffi::c_char,
    paths_len: usize,
) -> ffi::c_int {
    // Safety: The pointer is guaranteed to be created by the C API.
    let router = unsafe { &mut *(router.0 as *mut LookupRouter) };
    let paths = if paths.is_null() {
        return 0;
    } else {
        let mut p = Vec::with_capacity(paths_len);

        for i in 0..paths_len {
            // Safety: The pointer is guaranteed to be valid by the caller.
            if unsafe { paths.add(i).read() }.is_null() {
                return -libc::EINVAL;
            }
            // Safety: The pointer is guaranteed to be valid C string by the caller.
            let Ok(path) = unsafe { ffi::CStr::from_ptr(paths.add(i).read()) }.to_str() else {
                return -libc::EINVAL;
            };
            p.push(path);
        }

        p
    };

    retval::check!(router.attach(paths));

    0
}

/// Lookup the guest code in the router.
///
/// ## Safety
///
/// The provided pointers must be valid, otherwise it causes undefined behavior.
/// - `router` must be created by [`create`].
/// - `guest_sign` must be created by [`calc_sign`](../helper/fn.calc_sign.html).
/// - `path` must be a valid pointer to a null-terminated UTF-8 C string.
/// - `host_meta_ptr`, `host_meta_size`, `host_code_ptr`, and `host_code_size` must be
///    valid.
///
/// ## Returns
///
/// - `0` on success.
/// - `-EINVAL` if the argument is invalid:
///   - If `path` is null, or the C string is not valid UTF-8.
///   - If `guest_sign` or `host_meta_ptr` or `host_meta_size` or `host_code_ptr` or
///     `host_code_size` is null.
/// - `-ENOENT` if the cache is not found.
///
/// The error code could also come from the underlying implementation, i.e. from
/// [`LookupRouter::lookup`].
#[export_name = "cs2s_ro_lookup"]
pub unsafe extern "C" fn lookup(
    router: LookupRouterPtr,
    path: *const ffi::c_char,
    guest_addr: usize,
    guest_size: usize,
    guest_sign: *const CodeSign,
    host_meta_ptr: *mut *const ffi::c_void,
    host_meta_size: *mut usize,
    host_code_ptr: *mut *const ffi::c_void,
    host_code_size: *mut usize,
) -> ffi::c_int {
    // Safety: The pointer is guaranteed to be created by the C API.
    let router = unsafe { &mut *(router.0 as *mut LookupRouter) };
    if path.is_null()
        || guest_sign.is_null()
        || host_meta_ptr.is_null()
        || host_meta_size.is_null()
        || host_code_ptr.is_null()
        || host_code_size.is_null()
    {
        return -libc::EINVAL;
    }

    // Safety: The pointer is guaranteed to be valid C string by the caller.
    let Ok(path) = unsafe { ffi::CStr::from_ptr(path) }.to_str() else {
        return -libc::EINVAL;
    };

    // Safety: The `guest_sign` is guaranteed to be valid by the caller.
    let guest_sign = unsafe { &*guest_sign };

    if let Some(CacheTableVal {
        host_meta,
        host_code,
    }) = retval::check!(router.lookup(
        path,
        &CacheTableKeyRef {
            guest_addr,
            guest_size,
            guest_sign
        }
    )) {
        // Safety:
        // - The pointers `host_meta_size` and `host_meta_ptr` are guaranteed to be valid
        //   by the caller.
        // - The `host_meta` is valid and has long enough lifetime as it is from the
        //   global shared append-only cache.
        unsafe {
            host_meta_size.write(host_meta.len());
            host_meta_ptr.write(host_meta.as_ptr() as _);
        }

        // Safety:
        // - The pointers `host_code_size` and `host_code_ptr` are guaranteed to be valid
        //   by the caller.
        // - The `host_code` is valid and has long enough lifetime as it is from the
        //   global shared append-only cache.
        unsafe {
            host_code_size.write(host_code.len());
            host_code_ptr.write(host_code.as_ptr() as _);
        }
    } else {
        return -libc::ENOENT;
    }

    0
}

/// Cache data raw representation for C APIs.
#[repr(C)]
pub struct CacheTableDataRaw {
    pub guest_addr: usize,
    pub guest_size: usize,
    pub guest_sign: *const CodeSign,
    pub host_meta_len: usize,
    pub host_meta: *const u8,
    pub host_code_len: usize,
    pub host_code: *const u8,
}

/// Iterate over the cache blocks in the router. The passed callback function is called
/// for each cache block, with the provided `data` as the _1st_ argument and the cache
/// block as the _2nd_ argument.
///
/// ## Safety
///
/// The provided pointers must be valid, otherwise it causes undefined behavior.
/// - `router` must be created by [`create`].
/// - `path` must be a valid pointer to a null-terminated UTF-8 C string.
/// - `callback` must be a valid function pointer and safe to call.
///
/// The `callback` is safe to call if it does not call [`lookup`] with the same `router`
/// and `path` arguments.
///
/// In addition, the caller should ensure that during the [`for_each_blocks`] call, other
/// threads in the process do not call [`lookup`] with the same `router` and `path`.
///
/// ## Returns
///
/// - `0` on success.
/// - `-EINVAL` if the argument is invalid:
///   - If `path` is null, or the C string is not valid UTF-8.
///
/// The error code could also come from the underlying implementation, i.e. from
/// [`LookupRouter::iter_data`].
#[export_name = "cs2s_ro_for_each_blocks"]
pub unsafe extern "C" fn for_each_blocks(
    router: LookupRouterPtr,
    path: *const ffi::c_char,
    data: *mut ffi::c_void,
    callback: unsafe extern "C" fn(*mut ffi::c_void, *const CacheTableDataRaw),
) -> ffi::c_int {
    // Safety: The pointer is guaranteed to be created by the C API.
    let router = unsafe { &mut *(router.0 as *mut LookupRouter) };

    if path.is_null() {
        return -libc::EINVAL;
    }

    // Safety: The pointer is guaranteed to be valid C string by the caller.
    let Ok(path) = unsafe { ffi::CStr::from_ptr(path) }.to_str() else {
        return -libc::EINVAL;
    };

    retval::check!(router.iter_data(path)).for_each(|(key, val)| {
        let cache_data_raw = CacheTableDataRaw {
            guest_addr: key.guest_addr,
            guest_size: key.guest_size,
            guest_sign: key.guest_sign,
            host_meta_len: val.host_meta.len(),
            host_meta: val.host_meta.as_ptr(),
            host_code_len: val.host_code.len(),
            host_code: val.host_code.as_ptr(),
        };

        // Safety: The callback is guaranteed to be valid by the caller.
        unsafe { callback(data, &cache_data_raw) };
    });

    0
}

/// Destroy the lookup router.
///
/// ## Safety
///
/// The provided pointer must be valid, otherwise it causes undefined behavior.
/// - `router` must be created by [`create`].
///
/// After calling this function, the `router` should not be used.
#[export_name = "cs2s_ro_destroy"]
pub unsafe extern "C" fn destroy(router: LookupRouterPtr) {
    // Safety: The pointer is guaranteed to be created by the C API.
    drop(unsafe { Box::from_raw(router.0 as *mut LookupRouter) });
}

#[cfg(test)]
mod test {
    use std::{ffi::CString, mem, ptr, slice};

    use coverage_helper::test;
    use cs2s::route::HandleRouter;
    use cs2s_testdata::*;

    use super::*;

    #[test]
    fn basic_ops() {
        const PREFIX: &str = "ctest-route-basic-ops";

        // Create the server router.
        let mut server = HandleRouter::new(PREFIX);

        // Create the client router.
        //
        // Safety:
        // - The prefix is a valid UTF-8 C string.
        // - The handle pointer is valid.
        let client = unsafe {
            let mut router = mem::zeroed();
            let prefix = ffi::CString::new(PREFIX).unwrap();
            assert_eq!(create(prefix.as_ptr(), &mut router), 0);
            router
        };

        // Attach the paths.
        server.create(PATHS).unwrap();

        let mut path_array = [ptr::null(); PATHS.len()];
        let paths = PATHS
            .iter()
            .map(|&s| CString::new(s).unwrap())
            .collect::<Vec<_>>();
        for (i, path) in paths.iter().enumerate() {
            path_array[i] = path.as_ptr();
        }

        assert_eq!(
            // Safety: The router is created by the C API, and the paths are valid.
            unsafe { attach(client, path_array.as_ptr(), PATHS.len()) },
            0
        );

        // Attach the null paths.
        assert_eq!(
            // Safety: The router is created by the C API.
            unsafe { attach(client, ptr::null(), 0) },
            0
        );

        // Lookup the non-existing cache & synchronize it.
        for (i, ((path, guest_code), (host_meta, host_code))) in PATHS
            .iter()
            .zip(GUEST_CODES.iter())
            .zip(HOST_METAS.iter().zip(HOST_CODES.iter()))
            .enumerate()
        {
            let path_cstr = CString::new(*path).unwrap();

            let guest_addr = 0x400000 + i * 0x40;

            let mut host_meta_ptr = ptr::null();
            let mut host_meta_size = 0;
            let mut host_code_ptr = ptr::null();
            let mut host_code_size = 0;
            assert_eq!(
                // Safety:
                // - The router is created by the C API
                // - The `path_cstr` is a valid C string
                // - The `guest_code` is a valid code data with the size of `guest_size`
                // - The `host_meta_ptr`, `host_meta_size`, `host_code_ptr`, and
                //   `host_code_size` are valid
                unsafe {
                    lookup(
                        client,
                        path_cstr.as_ptr(),
                        guest_addr,
                        guest_code.len(),
                        &guest_code.into(),
                        &mut host_meta_ptr,
                        &mut host_meta_size,
                        &mut host_code_ptr,
                        &mut host_code_size,
                    )
                },
                -libc::ENOENT,
            );

            server
                .synchronize(
                    path,
                    &CacheTableKeyRef {
                        guest_addr,
                        guest_size: guest_code.len(),
                        guest_sign: &guest_code.into(),
                    },
                    &CacheTableVal {
                        host_meta,
                        host_code,
                    },
                )
                .unwrap();

            // Callback context.
            struct Ctx<'a> {
                guest_addr: usize,
                guest_size: usize,
                guest_sign: CodeSign,
                host_meta: &'a [u8],
                host_code: &'a [u8],
                found: bool,
            }
            let mut ctx = Ctx {
                guest_addr,
                guest_size: guest_code.len(),
                guest_sign: guest_code.into(),
                host_meta,
                host_code,
                found: false,
            };

            // Callback function.
            extern "C" fn callback(data: *mut ffi::c_void, cache_data: *const CacheTableDataRaw) {
                // Safety: The pointer just points to the context.
                let ctx = unsafe { &mut *(data as *mut Ctx) };

                // Safety: The provided pointer is guaranteed to be valid by design.
                let cache_data = unsafe { &*cache_data };

                if cache_data.guest_addr == ctx.guest_addr
                    && cache_data.guest_size == ctx.guest_size
                    // Safety: The pointer to the code sign is guaranteed to be valid by
                    // design.
                    && unsafe { &*cache_data.guest_sign } == &ctx.guest_sign
                {
                    assert_eq!(cache_data.host_meta_len, ctx.host_meta.len());
                    assert_eq!(cache_data.host_code_len, ctx.host_code.len());

                    // Safety: The provided pointers are valid and the lengths are
                    // correct.
                    let (cache_host_meta, cache_host_code) = unsafe {
                        (
                            slice::from_raw_parts(cache_data.host_meta, cache_data.host_meta_len),
                            slice::from_raw_parts(cache_data.host_code, cache_data.host_code_len),
                        )
                    };

                    assert_eq!(cache_host_meta, ctx.host_meta);
                    assert_eq!(cache_host_code, ctx.host_code);
                    ctx.found = true;
                }
            }

            // Safety:
            // - The router is created by the C API.
            // - The `path_cstr` is a valid C string.
            // - The `callback` is a valid function pointer.
            unsafe {
                for_each_blocks(
                    client,
                    path_cstr.as_ptr(),
                    &mut ctx as *mut _ as _,
                    callback,
                );
            }
            assert!(ctx.found);
        }

        // Lookup the existing cache.
        let path_cstr = CString::new(PATH_0).unwrap();
        let mut host_meta_ptr = ptr::null();
        let mut host_meta_size = 0;
        let mut host_code_ptr = ptr::null();
        let mut host_code_size = 0;
        assert_eq!(
            // Safety:
            // - The router is created by the C API
            // - The `path_cstr` is a valid C string
            // - The `GUEST_CODE_0` is a valid code data with the size of
            //   `GUEST_CODE_0.len()`
            // - The `host_meta_ptr`, `host_meta_size`, `host_code_ptr`, and
            //   `host_code_size` are valid
            unsafe {
                lookup(
                    client,
                    path_cstr.as_ptr(),
                    0x400000,
                    GUEST_CODE_0.len(),
                    &GUEST_CODE_0.into(),
                    &mut host_meta_ptr,
                    &mut host_meta_size,
                    &mut host_code_ptr,
                    &mut host_code_size,
                )
            },
            0,
        );
        // Safety: The host meta/code is guaranteed to be valid.
        let host_code =
            unsafe { slice::from_raw_parts(host_code_ptr as *const u8, host_code_size) };
        assert_eq!(host_code, HOST_CODES[0]);

        // Safety: The router is created by the C API.
        unsafe { destroy(client) };
    }

    #[test]
    fn invalid_args() {
        // Create the router with a null prefix.
        //
        // Safety: The prefix is null and the handle pointer is null. So we expect an
        // error return.
        assert!(unsafe { create(ptr::null(), ptr::null_mut()) } < 0);

        // Create the router with a non-UTF-8 prefix.
        let name = [b'a', 0xFF, 0];
        // Safety: The name is not a valid UTF-8 C string and the handle pointer is null.
        // So we expect an error return.
        assert!(unsafe { create(name.as_ptr() as _, ptr::null_mut()) } < 0);

        // Invalid lookup arguments.
        //
        // Safety: The prefix is a valid UTF-8 C string and the handle pointer is valid.
        let client = unsafe {
            let mut router = mem::zeroed();
            let prefix = ffi::CString::new("ctest-route-invalid-args").unwrap();
            assert_eq!(create(prefix.as_ptr(), &mut router), 0);
            router
        };

        // Lookup with null args.
        assert!(
            // Safety: The router is created by the C API, and some of the arguments are
            // null. So we expect an error return.
            unsafe {
                lookup(
                    client,
                    ptr::null(),
                    0,
                    0,
                    &[].into(),
                    ptr::null_mut(),
                    ptr::null_mut(),
                    ptr::null_mut(),
                    ptr::null_mut(),
                )
            } < 0
        );

        // For-each blocks with null path.
        extern "C" fn callback(_data: *mut ffi::c_void, _cache_data: *const CacheTableDataRaw) {}
        // Safety:
        // - The router is created by the C API.
        // - The `path` in null, so we expect an error return.
        // - The `callback` is a valid function pointer.
        assert!(unsafe { for_each_blocks(client, ptr::null(), ptr::null_mut(), callback) } < 0);
    }

    #[test]
    fn invalid_paths() {
        const PREFIX: &str = "ctest-route-invalid-paths";

        // Create the client router.
        //
        // Safety:
        // - The prefix is a valid UTF-8 C string.
        // - The handle pointer is valid.
        let client = unsafe {
            let mut router = mem::zeroed();
            let prefix = ffi::CString::new(PREFIX).unwrap();
            assert_eq!(create(prefix.as_ptr(), &mut router), 0);
            router
        };

        // Invalid paths with a null pointer.
        let paths = [ptr::null()];

        assert!(
            // Safety: The router is created by the C API, and the paths are invalid.
            unsafe { attach(client, paths.as_ptr(), paths.len()) } < 0
        );

        // Invalid paths with a non-UTF-8 string.
        let name = [b'a', 0xFF, 0];
        let paths = [name.as_ptr()];
        assert!(
            // Safety: The router is created by the C API, and the paths are invalid.
            unsafe { attach(client, paths.as_ptr() as _, paths.len()) } < 0
        );

        // Lookup with invalid path.
        let mut host_meta_ptr = ptr::null();
        let mut host_meta_size = 0;
        let mut host_code_ptr = ptr::null();
        let mut host_code_size = 0;
        assert!(
            // Safety:
            // - The router is created by the C API.
            // - The `name` is a valid C string.
            // - The code signature is calculated via standard API.
            // - The `host_meta_ptr`, `host_meta_size`, `host_code_ptr` and
            //   `host_code_size` are valid.
            unsafe {
                lookup(
                    client,
                    name.as_ptr() as _,
                    0x400000,
                    GUEST_CODE_0.len(),
                    &GUEST_CODE_0.into(),
                    &mut host_meta_ptr,
                    &mut host_meta_size,
                    &mut host_code_ptr,
                    &mut host_code_size,
                )
            } < 0
        );

        // For-each blocks with invalid path.
        extern "C" fn callback(_data: *mut ffi::c_void, _cache_data: *const CacheTableDataRaw) {}
        assert!(
            // Safety: The router is created by the C API.
            unsafe { for_each_blocks(client, name.as_ptr() as _, ptr::null_mut(), callback) } < 0
        );
    }
}
